Webpack学习(给力!篇幅较长)

您所在的位置:网站首页 webpack 打包html Webpack学习(给力!篇幅较长)

Webpack学习(给力!篇幅较长)

#Webpack学习(给力!篇幅较长)| 来源: 网络整理| 查看: 265

前端模块化成为了主流的今天,离不开各种打包工具的贡献。webpack就是杰出代表!以下是官网对webpack介绍图。文章主要介绍概念思路,简单配置,更齐全配置一定要到官网查阅。 进入学习之前建议先了解一下:npm使用与package.json说明

概述 webpack是什么

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

Webpack是一个开源的JavaScript模块打包工具,其最核心的功能是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个或多个bundle。这个过程就叫作模块打包。一般而言,一个入口对应一个bundle,但一个入口也可产出多个bundle(比如:使用了样式分离插件)。对于webpack来说,所有的资源(.js、.css、.png)都是module。

webpack与node或npm的关系

npm是于Node社区中产生的,是nodejs的官方包管理工具 , webpack是npm生态中的一个模块 。webpack是用来解决JavaScript模块之间的依赖,npm用来解决包之间依赖和版本的控制。

模块打包的作用

为什么不直接使用普通js等资源文件呢?干嘛要打包起来?

减少了网络请求连接 普通html文档引入了多个外部资源文件,那将会建立多个http请求连接,每个连接成本都是很大的。模块打包后,可以将多个资源文件合并成一个资源文件,从而减少了http请求。解决模块间的依赖关系 html文档中引入多个js文件,假如某两个js文件存在上下依赖关系,只能人为控制先引入哪个js文件后引入哪个js文件,这样很容易导致不清楚具体代码逻辑的人搞错。模块打包工具会分析模块间关系,帮我们维护这些依赖关系。多个模块之间的作用域是隔离的,彼此不会有命名冲突 每个script标签中,顶层作用域即全局作用域,如果没有任何处理而直接在代码中进行变量或函数声明,就会造成全局作用域的污染。 模块打包工具的工作方式 将存在依赖关系的模块按照特定规则合并为单个JS文件,一次全部加载进页面中。在页面初始时加载一个入口模块,其他模块异步地进行加载。 webpack相比于其他模块打包工具有何特长 Webpack默认支持多种模块标准,包括AMD、 Commonjs,以及最新的ES6模块,而其他工具往往只能兼容一到两种。Webpack有完备的代码分割( code splitting)解决方案。可以分割打包后的资源,首屏只加載必要的部分,不太重要的功能放到后面动态地加载。提升首页渲染速度。Webpack可以处理各种类型的资源。除了 Javascript以外, Webpack还可以处理样式、模板,甚至图片等。Webpack拥有庞大的社区支持。除了 Webpack核心库以外,还有无数开发者来为它编写周边插件和工具。 核心概念

有几个概念有必要先了解,方便更好的了解webpack。先看一个例子:

关键文件目录结构

project ├── src/ | ├── index.css | ├── index.js | ├── common.js | └── utils.js └── webpack.config.js

文件内容

//index.css body {background-color: red;} //utils.js export function square(x) { return x * x; } //common.js const {log}=console; module.exports = {log} //index.js import './index.css' import {log} from './common.js'

配置文件 webpack.config.js (看不懂没关系,后文会讲)

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports={ entry: { index: "./src/index.js", utils: './src/utils.js', }, output: { filename: "[name].bundle.js", // 输出 index.bundle.js 和 utils.bundle.js }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', // css-loader 负责解析 CSS 代码, 处理 CSS 中的依赖 ], }, ] }, plugins: [ // 用 MiniCssExtractPlugin 抽离出 css 文件 new MiniCssExtractPlugin({ filename: '[name].bundle.css' // 输出的 css 文件名为 index.css }), ] }

编译 产出文件目录结构

project ├── dist/ ├── index.bundle.css ├── index.bundle.js └── utils.bundle.js

以下几个是官网说的核心概念:

入口(entry)

资源打包的入口,Webpack从这里开始进行模块依赖的查找。指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。

依赖关系树 输出(output)

output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认输出路径为 ./dist。

预处理器(loader)

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。 主要属性:

test ,接受正则表达式,用于标识出需要被转换文件。use ,表示进行转换时,应该使用哪个 loader。 插件(plugins)

loader 被用于转换某些文件类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件可以完成loader完成不了的任务。

更多概念:module chunk bundle module

对于webpack来说,所有的资源(.js、.css、.png)都是module。webpack能直接处理的只有js文件,其他文件需要借助loader或者plugin。

chunk

chunk字面的意思是代码块,是webpack内部运行时的概念,在Webpack中可以理解成被抽象和包装过后的一些模块。

chunk是webpack根据功能拆分出来的,包含三种情况:

项目入口(entry)通过import()动态引入的代码通过splitChunks拆分出来的代码 bundle

由chunk得到的打包产物称之为bundle。一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出。

????????????????????图片来源:卤蛋实验室

一般来说一个 chunk 对应一个 bundle,比如上图中的 utils.js -> chunks 1 -> utils.bundle.js; 但也有例外,比如上述例子中, MiniCssExtractPlugin 从 chunks 0 中抽离出了 index.bundle.css 文件。

module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:

我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。

创建webpack项目 安装webpack 全局安装 npm install webpack webpack-cli -g

项目合作的时候,在自己电脑测试数据运行正常,换到别人电脑,假如webpack版本不一致,可能导致输出不一样。

项目内安装 npm install webpack webpack-cli --save-dev //npm i webpack webpack-cli -D

通过 webpack-cli 可以使用终端配置 webpack,一般一起安装使用。

--save-dev 把依赖保存在开发环境, i 是 install 简写 -D是--save-dev简写

建立第一个webpack项目 初始化项目 mkdir webpack-app cd webpack-app npm init -y

-y 可选参数,可以让你跳过对项目信息的填写

安装webpack npm i webpack webpack-cli -D 创建两个有依赖关系的js模块(暂且在根目录创建)

有依赖关系是为了方便查看模块打包后的效果。

//add_context.js export default function(){ document.write('Hello Webpack'); } //index.js import addContent from './add_content.js' document.write('Hello Joe'); addContent();

目前项目结构如下:

project ├── node_modules/ ├── add_content.js ├── index.js ├── package.json └── package-lock.json 进行模块打包 //入口文件为index.js 输出的整合文件名为bundle.js 运行模式为开发模式 npx webpack --entry=./js/index.js --output-filename=bundle.js --mode=development

npx 在node环境执行 webpack二进制文件(软件) 后面的就是对webpack的配置参数。npx webpack --help查看更多的webpack配置参数。

执行完,根目录下生成一个 dist 文件夹,里面装有 bundle.js 文件。

新建html并引入bundle.js看页面效果

根目录新建index.html

打开网页,你会看到Hello Joe Hello Webpack。

不想每次启动都写这么多:

package.json

"scripts": { "build": "webpack --entry=./index.js --output-filename=bundle.js --mode=development" },

以后就可以 npm run build这样启动

创建一个规范化的webpack项目

1.规范工程目录

//对上面入门案例文件目录进行修改 只写了关键几个 project ├── dist/ (存放项目的输出文件) ├── node_module/ (node资源文件) ├── src/ (存放项目的资源文件) | ├── index.js (webpack默认源代码的入口文件) | └── add_content.js ├── index.html ├── package.json (项目的描述文件 main:项目入口文件 dependencies:生产依赖 devDependencies:开发依赖) ├── package-lock.json └── webpack.config.js (webpack默认配置文件)

2.添加webpack配置文件

//webpack.config.js module.exports={ entry:'./src/index.js', output:{ filename:'bundle.js', }, mode:'development', }

3.修改package.json中对webpack的启动脚本

"build": "webpack"

如此,webpack将会自动寻找根目录下的webpack.config.js文件来启动webpack!之后npm run build即可。

webpack-dev-server 拒绝每次调试都要编译一次

之前每次改变资源文件都需要重新编译一次,甚是麻烦!

webpack-dev-serer是一个便捷的本地开发工具 ,有—项很便捷的特性就是live-reloading(自动刷新)。

1.安装

npm install webpack-dev-server --save-dev

2.配置到package.json的启动脚本中

"scripts": { "build": "webpack", "dev":"webpack-dev-server" },

3.webpack配置文件添加对webpack-dev-server的配置

module.exports={ entry:'./src/index.js', output:{ filename:'bundle.js', }, mode:'development', devServer:{ publicPath:'/dist' } }

4.启动看效果

rpn run dev

可见服务发布在本地8080端口,需要通过该端口访问才能看到自动刷新的效果,直接打开index.html文件是没有这个效果的。因为webpack-dev-server并没有打包输出,而是将编译结果写在内存中,方便迅速刷新调试。

启动报错 Cannot find module 'webpack-cli/bin/config-yargs’ 貌似高版本的webpack-cli没有config-yargs模块了,我把它卸载npm uninstall webpack-cli了,然后安装了npm i [email protected] -D版本即可!

资源输入输出 资源处理流程

在一切流程的最开始,我们需要指定一个或多个入口(entry),也就是告诉Webpack具体从源码目录下的哪个文件开始打包。如果把工程中各个模块的依赖关系当作一棵树,那么入口就是这棵依赖树的根。这些存在依赖关系的模块会在打包时被封装为—个chunk,打包后生成的就是bundle。

配置资源入口

webpack通过context和entry这两个配置项来共同决定入口文件的路径。在配置入口时,实际上做了两件事:

确定入口模块位置,告诉Webpack从哪里开始进行打包。定义chunk name。如果工程只有一个入口,那么chunk 默认名为“main”; 如果工程有多个入口,我们需要为每个入口定义chunk name,来作为该chunk的标识。一般而言,chunk name也是bundle name,但bundle name 往往重新定义输出。 context

配置context的主要目的是让entry的编写更加简洁,尤其是在多入口的情况下。context可以省略,默认值为当前工程的根目录。context+entry的路径拼接成资源的完整路径。

entry

主讲数组类型和对象类型入口。

📌对象形式

如果想要定义多入口,则必须使用对象的形式。对象的属性名(key)是chunk name,属性值(value)是入口路径。

module.exports={ entry:{ index:'./src/index.js', other:'./src/other.js' } }

📌数组形式

传入一个数组的作用是将多个资源预先合并,在打包时webpack会将entry数组中的最后一个元素作为实际的入口路径。

module.exports={ entry:['./src/other_entry.js','./src/index.js'], }

等价于

//index.js import other_entry.js module.exports={ entry:'./src/index.js', } 优化资源入口 vendor

vendor是用来提取公共且很少发生变化的模块,在webpack中vendor一般指的是工程所使用的库、框架等第三方模块集中打包而产生的bundle。

若工程只产生一个JS文件并且它的体积很大,一旦产生代码更新,用户都要重新下载整个资源文件,这对于页面的性能是非常不友好的。

entry:{ app:'./src/index.js', vendor:['react','react-dom','react-router'], },

通过这样的配置,app bundle将只包含业务模块,其依赖的第三方模块将会被抽取出来生成一个新的bundle。由于vendor仅仅包含第三方模块,这部分不会经常变动,因此可以有效地利用客户端缓存,在用户后续请求页面时会加快整体的渲染速度。

配置资源出口 filename //多入口多输出 module.exports={ entry:{ index:'./src/index.js', other:'./src/other.js' } output:{ filename:'[name].js', }, }

[name]会被代替为chunk name,还有以下几种模板变量也可以部署到filename配置中。 上述变量一般有如下两种作用:

区分chunk。控制客户端缓存。表中的[hash]和[chunkhash]都与chunk内容直接相关,在filename中使用了这些变量后,当chunk的内容改变时,可以同时引起资源文件名的更改,客户端检查发现请求文件chunkhash发生变化就下载新文件。[query]也可以起到类似的效果,只不过它与chunk内容无关,要由开发者手动指定。

如果要控制客户端缓存,最好还要加上[chunkhash],因为每个chunk所产生的[chunkhash]只与自身内容有关,单个chunk内容的改变不会影响其他资源,可以最精确地让客户端缓存得到更新。

hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。采用hash计算的话,每一次构建后生成的哈希值都不一样,即使文件内容压根没有改变。这样子是没办法实现缓存效果

output:{ filename:'[name].[chunkhash].js', },

path

用来指定资源输出的位置,要求必须是绝对路径.

输出位置:打包完成后资源产生的目录,一般将其指定为工程中的dist目录。

output:{ filename:'[name].js', path:path.resolve(__dirname,'dist'), },

在Webpack 4之后,output.path已经默认为dist目录,除非我们需要更改它,否则不必单独配置。

publicPath

用来指定资源的请求位置.(默认值为空字符串'') 请求位置:由JS或CSS所请求的间接资源路径。

output中的publicPath用于给生成的静态资源路径添加前缀,也就是会在打包生成的html文件里面引用资源路径中添加前缀。

📌 页面中的资源分为两种 :直接资源 间接资源

直接资源:由HTML页面直接请求的,比如通过标签加载的JS; 间接资源:由JS或CSS请求的,如异步加载的JS、从CSS请求的图片字体等。

publicPath的作用就是指定这部分间接资源的请求位置。

首屏加载的JS资源地址是通过页面中的来指定的,而间接资源(通过首屏JS再进一步加载的JS)的位置则要通过output.publicPath来指定。比如import('./bar.js')使bar.js成为了一个间接资源,我们需要配置publicPath来告诉Webpack去哪里获取它。

publicPath有3种形式:

1.html相关的

与HTML相关,也就是说我们可以将publicPath指定为HTML的相对路径,在请求这些资源时会以当前页面HTML所在路径加上相对路径,构成实际请求的URL。

// 假设当前HTML地址为 https://example.com/app/index.html // 在该页面异步加载的资源名为 0.chunk.js publicPath: "" // 实际路径https://example.com/app/0.chunk.js publicPath: "./js" // 实际路径https://example.com/app/js/0.chunk.js publicPath: "../assets/" // 实际路径https://example.com/aseets/0.chunk.js

2.host相关的

若publicPath的值以/开始,则代表此时publicPath是以当前页面的host name为基础路径的。

// 假设当前HTML地址为 https://example.com/app/index.html // 异步加载的资源名为 0.chunk.js publicPath: "/" // 实际路径https://example.com/0.chunk.js publicPath: "/js/" // 实际路径https://example.com/js/0.chunk.js publicPath: "/dist/" // 实际路径https://example.com/dist/0.chunk.js

3.CDN相关的

绝对路径的形式,一般发生于静态资源放在CDN上面时,因为域名不一样

// 假设当前页面路径为 https://example.com/app/index.html // 异步加载的资源名为 0.chunk.js publicPath: "http://cdn.com/" // 实际路径http://cdn.com/0.chunk.js publicPath: "https://cdn.com/" // 实际路径https://cdn.com/0.chunk.js publicPath: "//cdn.com/assets/" 实际路径 //cdn.com/assets/0.chunk.js 拓展:devServer 中 publicPath

webpack-dev-server下也有publicPath配置,是针对webpack-dev-server的静态资源服务路径。

devServer里面的publicPath表示的是此路径下的打包文件可在浏览器中访问,若是devServer里面没有设置publicPath,则会认可是output里面设置的publicPath的值。

const path = require('path'); module.exports = { entry: './src/app.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'dist'), }, devServer: { publicPath: '/assets/', port: 3000, }, };

启动webpack-dev-server的服务后,访问localhost:3000/dist/bundle.js时却会得到404。这是因为devServer.publicPath配置项将资源位置指向了localhost:3000/assets/,因此只有访问localhost:3000/assets/bundle.js才能得到我们想要的结果。

将webpack-dev-server的publicPath与Webpack中的output.path保持一致,这样在任何环境下资源输出的目录都是相同的。

loader 一切皆模块

一个Web工程通常会包含HTML、JS、CSS、模板、图片、字体等多种类型的静态资源,并且这些资源之间都存在着某种联系。比如,JS文件之间有互相依赖的关系,在CSS中可能会引用图片和字体等。对于Webpack来说,所有这些静态资源都是模块,可以像加载一个JS文件一样去加载它们。

loader

装载器(loader),它赋予了Webpack可处理不同资源类型的能力,如HTML、CSS、模板、图片、字体等,极大丰富了其可扩展性。

webpack本身只认识JavaScript,对于其他类型的资源必须预先定义一个或多个loader对其进行转译,输出为webpack能够接收的形式再继续进行,因此loader做的实际上是一个预处理的工作。loader本身只是编译核心库与webpack的连接器,有时还需要为loader补充额外的库。类似于我们装babel-loader时还要安装babel-core;编译sass除了sass-loader以外还要安装node-sass,node-sass是真正用来编译SCSS的,而sass-loader只是起到黏合的作用。

loader 执行顺序

webpack中的loader按照执行顺序可分为pre、inline、normal、post四种类型,我们直接定义的loader都属于normal类型(从后往前),inline形式官方已经不推荐使用,而pre和post则需要使用enforce配置项来指定。

module:{ rules:[{ test:/\.css$/, use:['style-loader','css-loader'], }] }

use 数组是逆序加载的,因此要把最后生效的loader放在use数组最前面。

loader 的使用案例

比如:在js文件引入css文件

import './style.css'; 安装css-loader

为了能在js文件引入css模块,第一步要把css-loader加到工程中。

npm i css-loader -D 配置loader,将css-loader引入工程中 module:{ rules:[{ test:/\.css$/, use:['css-loader'], }] }

与loader相关的配置都在module对象中,其中module.rules代表了模块的处理规则。test可接收一个正则表达式,use可接收一个数组,数组包含该规则所使用的loader。/\.css$/匹配所有以.css结尾的文件。

此时,CSS的样式仍然没有在页面上生效。这是因为css-loader的作用仅仅是处理CSS的各种加载语法(@import和url()函数等),如果要使样式起作用还需要style-loader来把样式插入页面。

把style-loader加到工程 npm i style-loader -D 将style-loader引入工程中 module:{ rules:[{ test:/\.css$/, use:['style-loader','css-loader'], }] }

把style-loader加到了css-loader前面,这是因为在webpack打包时是将资源按照use数组逆序传给loader处理的,因此要把最后生效的放在前面。否则报错。

npm run build模块打包,发现生成的 dist 文件夹里并没有css文件,因为样式一并打包到入口的bundle.js中了。原本你在普通html文档中,只要引入这个bundle.js 就可同时兼具样式的效果了。

样式预处理

样式预编译语言,如Sass(scss)、Less等。 sass,less编译后可生成css。

sass

1.安装

npm install sass-loader node-sass

发现node-sass下载太慢,后面直接就是连不上,将node-sass库设为阿里源的node-sass库!

npm config set sass_binary_site=https://npm.taobao.org/mirrors/node-sass/

2.配置

module: { rules: [ { test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'], } ], },

3.测试运行

//style.scss $color='red' body{ color: $color; } //index.js import './style.scss'

打包后生成的css文件

body{ color: ‘red’; } less npm install less-loader less

其他步骤雷同

sourceMap

背景:我们在打包中,将开发环境中源代码经过压缩,去空格,babel编译转化,最终可以得到适用于生产环境的项目代码,这样处理后的项目代码和源代码之间差异性很大,会造成无法debug的问题。 生产环境的代码都是经过压缩处理等的,调试的时候只能定位到压缩处理后的代码的位置,无法定位到开发环境中对应的源代码所在位置。

sourcemap就是为了解决上述代码定位的问题,简单理解,就是构建了处理前的代码和处理后的代码之间的桥梁。主要是方便开发人员的错误定位。这里的处理操作包括: I)压缩,减小体积 II)将多个文件合并成同一个文件 III)其他语言编译成javascript,比如TypeScript和CoffeeScript等

假如我们想要在浏览器的调试工具里查看源码,需要分别为sass-loader和css-loader单独添加source map的配置项。

rules: [ { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { sourceMap: true, }, }, { loader: 'sass-loader', options: { sourceMap: true, }, } ], } ], loader的其他配置项 exclude include

exclude与include是用来排除或包含指定目录下的模块,可接收正则表达式或者字符串(文件绝对路径),以及由它们组成的数组。

module:{ rules:[{ test:/\.css$/, use:['style-loader','css-loader'], exclude:/node_modules/ }] }

node_modules模块不会执行这条规则,避免遍历该模块,加快打包速度.

同时配置exclude include ,exclude 优先级高

resource issuer

在Webpack中,被加载模块是resource,而加载者是issuer。

// index.js import './style.css';

resource为/path/of/app/style.css,issuer是/path/of/app/index.js。

配置项test、exclude、include本质上属于对resource也就是被加载者的配置。

只有在/src/pages/目录下面的JS文件引用了CSS文件,这条rule才会生效

rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'], exclude: /node_modules/, issuer: { test: /\.js$/, include: /src/pages/, }, } ],

上述可读性较差,以下是等价的形式

{ use: ['style-loader', 'css-loader'], resource: { test: /\.css$/, exclude: /node_modules/, }, issuer: { test: /\.js$/, exclude: /node_modules/, }, } ], enforce

webpack中的loader按照执行顺序可分为pre、inline、normal、post四种类型,上面我们直接定义的loader都属于normal类型(从后往前),inline形式官方已经不推荐使用,而pre和post则需要使用enforce来指定。

enforce用来指定一个loader的种类,只接收“pre”或“post”两种字符串类型的值。

“pre”,代表它将在所有正常loader之前执行 “post”,代表在所有loader之后执行

rules: [ { test: /\.js$/, enforce: 'pre', use: 'eslint-loader', } ],

在配置中添加了一个eslint-loader来对源码进行质量检测,其enforce的值为“pre”,代表它将在所有正常loader之前执行。

常用loader介绍 babel-loader npm install babel-loader @babel/core @babel/preset-env -D rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { cacheDirectory: true, presets: [[ 'env', { modules: false, } ]], }, }, } ],

由于@babel/preset-env会将ES6 Module转化为CommonJS的形式,这会导致Webpack中的tree-shaking特性失效,将@babel/preset-env的modules配置项设置为false会禁用模块语句的转化,而将ES6 Module的语法交给Webpack本身处理。

babel-loader支持从.babelrc文件读取Babel配置,因此可以将presets和plugins从Webpack配置文件中提取出来,也能达到相同的效果。

html-loader

html-loader用于将HTML文件转化为字符串并进行格式化,这使得我们可以把一个HTML片段通过JS加载进来。

npm install html-loader -D rules: [ { test: /\.html$/, use: 'html-loader', } ], // header.html h1>This is a Header. // index.js import headerHtml from './header.html'; document.write(headerHtml); file-loader

file-loader用于打包文件类型的资源(图片等),并返回其publicPath。

npm install file-loader -D rules: [ { test: /\.(png|jpg|gif)$/, use: 'file-loader', } ],

上面我们对png、jpg、gif这类图片资源使用file-loader,然后就可以在JS中加载图片了。

import avatarImage from './avatar.jpg'; console.log(avatarImage); // c6f482ac9a1905e1d7d22caa909371fc.jpg url-loader

url-loader与file-loader作用类似,唯一的不同在于用户可以设置一个文件大小的阈值,当大于该阈值时与file-loader一样返回publicPath,而小于该阈值时则返回文件base64形式编码。

npm install url-loader -D rules: [ { test: /\.(png|jpg|gif)$/, use: { loader: 'url-loader', options: { limit: 10240, name: '[name].[ext]', publicPath: './assets-path/', }, }, } import avatarImage from './avatar.jpg'; console.log(avatarImage); // data:image/jpeg;base64,/9j/2wCEAAgGBg…… 插件

webpack具有大量的插件支持,详细看webpack plugins 以下只对几个常用插件做介绍。

mini-css-extract-plugin (样式分离)

该插件将CSS提取到单独的文件中。

1.安装

npm i mini-css-extract-plugin -D

2.创建css文件

project ├── src/ | ├── common.css | ├── index.css | └── index.js ├── package.json └── webpack.config.js //common.css body{text-align: center;} //index.css body{background-color: #00FFFF;} //index.js import './common.css' import './index.css'

3.配置

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { entry: './app.js', output: { filename: '[name].js', }, mode: 'development', module: { rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader,'css-loader'], }], }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].css', }) ], };

4.运行

npx webpack

产出 main.css main.js

project ├── dist/ ├── main.css └── main.js //main.css body{text-align: center;} body{ background-color: #00FFFF; }

被index.js引入的两个css文件合并为一了。

terser-webpack-plugin (压缩JS) ~tree shaking

Webpack 4中默认使用的压缩JavaScript的插件为terser-webpack-plugin。从Webpack 4之后,这项配置被移到了config.optimization.minimize。(如果开启了mode:production,则不需要人为设置)。

1.默认启动

mode:'production'

2.手动启动

optimization: { minimize: true, },

3.自定义配置

npm i -D uglifyjs-webpack-plugin

webpack.config.js

const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimize: true, minimizer: [ // 覆盖默认的 minimizer new TerserPlugin({ /* your config */ test: /\.js(\?.*)?$/i, exclude: /\/excludes/, extractComments: true,//提取注释到一个独立文件 }) ], } } optimize-css-assets-webpack-plugin (压缩css)

样式分离后才可压缩css。

npm i optimize-css-assets-webpack-plugin -D

webpack.config.js

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); module.exports = { optimization: { minimizer: [new OptimizeCSSAssetsPlugin({ // 生效范围,只压缩匹配到的资源 assetNameRegExp: /\.optimize\.css$/g, // 压缩处理器,默认为 cssnano cssProcessor: require('cssnano'), // 压缩处理器的配置 cssProcessorOptions: { discardComments: { removeAll: true } }, // 是否展示 log canPrint: true, })], }, }; compression-webpack-plugin (资源压缩)

这个和上面的压缩不同,这个是使用算法压缩,改变原有的文件格式,前端接收数据后需要解压。上面的2种压缩只是去空白,死代码等,打包后还是原来资源的格式。

npm i compression-webpack-plugin -D

webpack.config.js

const CompressionWebpackPlugin = require('compression-webpack-plugin') module.exports = { plugins: [ new CompressionWebpackPlugin({ filename: '[path][name].gz[query]', algorithm: 'gzip', test: /\.(js|css|json|ttf)(\?.*)?$/i,//压缩了代码和字体 threshold: 0, minRatio: 0.8, }), ] }; html-webpack-plugin (生成注入依赖的html文件)

html-webpack-plugin插件用于简化创建HTML文件,它会在body中用script标签来包含我们生成的所有bundles文件。不需要我们手动引入。

该插件的两个主要作用:

为html文件中引入的外部资源如script、link动态添加每次compile后的hash,防止引用缓存的外部文件问题,也不用每次打包后手动引入发生变化的资源文件(加了hash命名的文件)

可以生成创建html入口文件,比如单页面可以生成一个html文件入口,配置N个html-webpack-plugin可以生成N个页面入口

将 webpack中entry配置的相关入口chunk 和 mini-css-extract-plugin抽取的css样式 插入到该插件提供的template或者templateContent配置项指定的内容基础上生成一个html文件,具体插入方式是将样式link插入到head元素中,script插入到head或者body中。

npm i html-webpack-plugin -D var HtmlWebpackPlugin = require('html-webpack-plugin') webpackconfig = { ... plugins: [ new HtmlWebpackPlugin() ] } Webpack App

输出的资源名绑定了chunkhash之后,资源改变,打包时资源名也会改变。 html-webpack-plugin会自动地将我们打包出来的资源名放入生成的[chunkname].html中,这样我们就不必手动地更新资源URL了。

配置多个html页面

... plugins: [ new HtmlWebpackPlugin({ template: 'src/html/index.html', excludeChunks: ['list', 'detail'] }), new HtmlWebpackPlugin({ filename: 'list.html', template: 'src/html/list.html', chunks: ['common', 'list'] }), new HtmlWebpackPlugin({ filename: 'detail.html', template: 'src/html/detail.html', chunks: ['common', 'detail'] }) ] ...

我们也可以传入一个已有的HTML模板

Custom Title app

text content

// webpack.config.js new HtmlWebpackPlugin({ template: './template.html', })

html-webpack-plugin详解

概念补充 mode

提供mode配置选项将告诉webpack采用相应环境下的内置优化。 有三个值:development ||production || none(退出任何默认优化选项)

development 模式下默认优化项

// webpack.development.config.js module.exports = { mode: 'development' devtool: 'eval', cache: true, performance: { hints: false }, output: { pathinfo: true }, optimization: { moduleIds: 'named', chunkIds: 'named', mangleExports: false, nodeEnv: 'development', flagIncludedChunks: false, occurrenceOrder: false, concatenateModules: false, splitChunks: { hidePathInfo: false, minSize: 10000, maxAsyncRequests: Infinity, maxInitialRequests: Infinity, }, emitOnErrors: true, checkWasmTypes: false, minimize: false, removeAvailableModules: false }, plugins: [ new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }), ] }

production 模式下默认优化项

// webpack.production.config.js module.exports = { mode: 'production', performance: { hints: 'warning' }, output: { pathinfo: false }, optimization: { moduleIds: 'deterministic', chunkIds: 'deterministic', mangleExports: 'deterministic', nodeEnv: 'production', flagIncludedChunks: true, occurrenceOrder: true, concatenateModules: true, splitChunks: { hidePathInfo: true, minSize: 30000, maxAsyncRequests: 5, maxInitialRequests: 3, }, emitOnErrors: false, checkWasmTypes: true, minimize: true, }, plugins: [ new TerserPlugin(/* ... */), new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }), new webpack.optimize.ModuleConcatenationPlugin(), new webpack.NoEmitOnErrorsPlugin() ] } tree shaking

tree shaking 树抖动,就会掉死叶子。在代码中,就是去无用代码的意思。

tree shaking本身只是为死代码添加上标记,真正去除死代码是通过压缩工具来进行的。使用我们前面介绍过的terser-webpack-plugin即可。在Webpack 4之后的版本中,直接mode:production也可以达到相同的效果。

ES6 Module依赖关系的构建是在代码编译时而非运行时。tree shaking功能,它可以在打包过程中帮助我们检测工程中没有被引用过的模块,这部分代码将永远无法被执行到,因此也被称为“死代码”。Webpack会对这部分代码进行标记,并在资源压缩时将它们从最终的bundle中去掉。

// index.js import { foo } from './util'; foo(); // util.js export function foo() { console.log('foo'); } export function bar() { // 没有被任何其他模块引用,属于“死代码” console.log('bar'); }

在Webpack打包时会对bar()添加一个标记,在正常开发模式下它仍然存在,只是在生产环境的压缩那一步会被移除掉。

模块热替换(Hot Module Replacement,HMR)

热更新 在我们每次改变代码,或者资源文件的时候,整个页面其实都会刷新。 热替换,直接替换更改后的依赖模块,而不用刷新整个页面,可以简单理解成局部更新。

许多Web开发框架和工具都提供了live reload来做热更新。模块热替换是 webpack 提供的最有用的功能之一。HMR对于大型应用尤其适用。试想一个复杂的系统每改动一个地方都要经历资源重构建、网络请求、浏览器渲染等过程,怎么也要几秒甚至几十秒的时间才能完成;

项目是基于webpack-dev-server开发时才可以启动模块热替换

const webpack = require('webpack'); module.exports = { // ... plugins: [ new webpack.HotModuleReplacementPlugin() ], devServer: { hot: true, }, };

Webpack会为每个模块绑定一个module.hot对象,这个对象包含了HMR的API。

调用HMR API有两种方式,一种是手动地添加这部分代码;另一种是借助一些现成的工具,比如react-hot-loader、vue-loader等。

// index.js import { add } from 'util.js'; add(2, 3); if (module.hot) { module.hot.accept(); }

index.js是应用的入口,那么我们就可以把调用HMR API的代码放在该入口中,这样HMR对于index.js和其依赖的所有模块都会生效。当发现有模块发生变动时,HMR会使应用在当前浏览器环境下重新执行一遍index.js(包括其依赖)的内容,但是页面本身不会刷新。

webpack打包原理

webpack打包原理是根据文件间的依赖关系对其进行静态分析,然后将这些模块按指定规则生成静态资源,当 webpack 处理程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

webpack有两种组织模块的依赖方式,同步、异步。异步依赖将作为分割点,形成一个新的块;在优化了依赖树之后,每一个异步区块都将作为一个文件被打包。

生产环境调优 打包优化 样式分离 MiniCssExtractPlugin

通过loader可以将样式文件引入到js文件中,模块打包后输出的还是js文件(样式也直接写入到该js文件了)。此时,哪怕只是修改了js部分的代码,那么css模块也会被重新打包;或者只修改了css文件,js文件是没有变化的,但是他们都是在一个bundle中,所以都会被认为都有修改。

常用的分离样式的插件有两个:extract-text-webpack-plugin(webpack4以下) 和 mini-css-extract-plugin(webpack4以上)

我使用的"webpack": “^5.10.1”,想试用extract-text-webpack-plugin,结果折腾半天也是报错,罢了罢了,反正过期货(估计已经被版本pass掉了),官方推崇的也是mini-css-extract-plugin。

上文有对mini-css-extract-plugin的介绍了。

代码分片 SplitChunks

代码分片(code splitting)可以把代码按照特定的形式进行拆分,按需加载。用户每次只加载必要的资源,优先级不太高的资源则采用延迟加载等技术渐进式地获取,这样可以保证页面的首屏速度。

手动提取公共模块

在Webpack中每个入口(entry)都将生成一个对应的资源文件,一些库和工具是不常变动,可以把它们放在一个单独的入口中,因此可以有效地利用客户端缓存,让用户不必在每次请求页面时都重新加载。(上文的vendor)

// webpack.config.js entry: { app: './app.js',//业务逻辑代码 常变 lib: ['lib-a', 'lib-b', 'lib-c']//工具类库 少变 }, output: { filename: '[name].[chunkcode].js', }, // index.html

lib.1ac59013ea124.js chunkcode根据文件内容生成的,内容不变,这个也不变,这样就不会重新打包生成!

以后不必担心引入这个生成的名麻烦的问题,有html-webpack-plugin帮解决,下文会讲到。

自动提取公共模块 optimization.SplitChunks

SplitChunks是Webpack 4内部自带的插件,用于将多个Chunk中公共的部分提取出来。

提取公共模块的好处:

开发过程中减少了重复模块打包,可以提升开发速度;减小整体资源体积;合理分片后的代码可以更有效地利用客户端缓存。

🌰案例对证

1.模块不作提取时

//index.js import React from 'react' document.write('index'+React.version) //other.js import React from 'react' document.write('other'+React.version) //webpack.config.js const path=require('path') module.exports={ context:path.resolve(__dirname,'./src'), entry:{ index:'./index.js', other:'./other.js', }, output:{ filename:'[name].js', }, }

可见index.js和other.js都分别加载了react模块。

2.使用SplitChunks提取公共模块 SplitChunks是在webpack的优化项中的,查看更多优化项webpack.optimization

//webpack.config.js optimization:{ splitChunks:{ chunks:'all', } }

chunks的值为all,这个配置项的含义是,SplitChunks将会对所有的chunks生效。默认情况下,SplitChunks只对异步chunks,比如import('xx.js')生效。

ok!看下打包结果:

project ├── dist/ ├── vendors-node_modules_react_index_js.js ├── index.js └── other.js

多生成一个文件vendors-node_modules_react_index_js.js,这个文件存储了原index.js和other.js的公共模块react。

webpack5自动分块需要满足以下条件:

可以共享新块,或者模块来自node_modules文件夹新的块将大于20kb(在min + gz之前)按需加载块时并行请求的最大数量将小于或等于30初始页面加载时并行请求的最大数量将小于或等于30 当试图满足最后两个条件时,最好使用较大的块。

如果提取后的资源体积太小,那么带来的优化效果也比较一般。

同时加载过多的资源,每一个请求都要花费建立链接和释放链接的成本,更多需要花费更多时间和性能,为此做出了平衡。

看下splitChunks默认配置便可知:

splitChunks: { chunks: 'async', minSize: 20000, minRemainingSize: 0, maxSize: 0, minChunks: 1, maxAsyncRequests: 30, maxInitialRequests: 30, automaticNameDelimiter: '~', enforceSizeThreshold: 50000, cacheGroups: { defaultVendors: { test: /[\\/]node_modules[\\/]/, priority: -10, reuseExistingChunk: true }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } } }

(1)SplitChunks工作模式 chunks配置SplitChunks的工作模式。它有3个可选值,分别为async(默认)、initial和all。async即只提取异步chunk,initial则只对入口chunk生效(如果配置了initial则上面异步的例子将失效),all则是两种模式同时开启。 (2)匹配条件 minSize、minChunks、maxAsyncRequests、maxInitialRequests都属于匹配条件 (3)命名 配置项name默认为true,它意味着SplitChunks可以根据cacheGroups和作用范围自动为新生成的chunk命名,并以automaticNameDelimiter分隔。如vendorsab~c.js意思是cacheGroups为vendors,并且该chunk是由a、b、c三个入口chunk所产生的。据在webpack5.10.3测试,automaticNameDelimiter并不生效 (4)cacheGroups 可以理解成分离chunks时的规则。默认情况下有两种规则——vendors和default。vendors用于提取所有node_modules中符合条件的模块,default则作用于被多次引用的模块。我们可以对这些规则进行增加或者修改,如果想要禁用某种规则,也可以直接将其置为false。当一个模块同时符合多个cacheGroups时,则根据其中的priority配置项确定优先级。

异步加载 import()

将一些暂时使用不到的模块延迟加载,初次渲染的时候用户下载的资源尽可能小,后续的模块等到恰当的时机再去触发加载。因此一般也把这种方法叫作按需加载。

//bar.js export function add(a,b){ return a+b; } //foo.js import('./bar.js').then(({add})=>{console.log(add(1,2))})

webpack.config.js

const path=require('path') module.exports={ context:path.resolve(__dirname,'./src/js/'), entry:{ foo:'./foo.js', bar:'./bar.js', }, output:{ filename:'[name].[chunkhash].js', publicPath:'/dist' }, mode:'development', }

首屏加载的JS资源地址是通过页面中的script标签来指定的,而间接资源(通过首屏JS再进一步加载的JS)的位置则要通过output.publicPath来指定。上面我们的import()相当于使bar.js成为了一个间接资源,我们需要配置publicPath来告诉Webpack去哪里获取它。

观察Network面板: Initiator指向的是当前资源的的启动文件。

可以发现bar.js是由foo.js发起请求得到的结果,并且是在foo.js加载完后才加载的,实际是异步加载的。

代码压缩

上文插件部分分别介绍了js css gzip压缩!不累赘

缩小打包范围 exclude和include

当exclude和include规则有重叠的部分时,exclude的优先级更高。

🌰include使babel-loader只生效于源码目录/src/scripts。

module: { rules: [ { test: /\.js$/, include: /src\/scripts/, loader: 'babel-loader, } ], }, noParse

不去解析但仍会打包到bundle中.

有些库我们是希望Webpack完全不要去进行解析的,即不希望应用任何loader规则,库的内部也不会有对其他模块的依赖,那么这时可以使用noParse对其进行忽略。

module.exports = { //... module: { noParse: /lodash/, } };

上面的配置将会忽略所有文件名中包含lodash的模块,这些模块仍然会被打包进资源文件,只不过Webpack不会对其进行任何解析。

IgnorePlugin

exclude和include是确定loader的规则范围,noParse是不去解析但仍会打包到bundle中。IgnorePlugin,它可以完全排除一些模块,被排除的模块即便被引用了也不会被打包进资源文件中。

一些由库产生的额外资源我们用不到但又无法去掉,因为引用的语句处于库文件的内部。比如,Moment.js是一个日期时间处理相关的库,为了做本地化它会加载很多语言包,对于我们来说一般用不到其他地区的语言包,但它们会占很多体积,这时就可以用IgnorePlugin来去掉。

plugins: [ new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, // 匹配资源文件 contextRegExp: /moment$/, // 匹配检索目录 }) ], Cache

有些loader会有一个cache配置项,用来在编译代码后同时保存一份缓存,在执行下一次编译前会先检查源码文件是否有变化,如果没有就直接采用缓存,也就是上次编译的结果。这样相当于实际编译的只有变化了的文件,整体速度上会有一定提升。

在Webpack 5中添加了一个新的配置项“cache:{type:"filesystem"}”,它会在全局启用一个文件缓存。要注意的是,该特性目前仅仅是实验阶段,并且无法自动检测到缓存已经过期。目前的解决办法就是,当我们更新了任何node_modules中的模块或者Webpack的配置后,手动修改cache.version来让缓存过期。

开发环境调优 优化构建 happyPack

在打包过程中有一项非常耗时的工作,就是使用loader将各种资源进行转译处理。

工作流程概括如下: 1)从配置中获取打包入口; 2)匹配loader规则,并对入口模块进行转译; 3)对转译后的模块进行依赖查找(如a.js中加载了b.js和c.js); 4)对新找到的模块重复进行步骤2)和步骤3),直到没有新的依赖模块。

从步骤2)到步骤4)是一个递归的过程,此处的Webpack是单线程的。这里的问题在于Webpack是单线程的,假设一个模块依赖于几个其他模块,Webpack必须对这些模块逐个进行转译。虽然这些转译任务彼此之间没有任何依赖关系,却必须串行地执行。

HappyPack是一个通过多线程来提升Webpack打包速度的工具。适用于那些转译任务比较重的工程,类似babel-loader和ts-loader效果更佳。

npm i -D happypack 单个loader优化 const HappyPack = require('happypack'); module.exports = { //... module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: 'happypack/loader', } ], }, plugins: [ new HappyPack({ loaders: [ { loader: 'babel-loader', options: { presets: ['react'], }, } ], }) ], };

使用happypack/loader替换了原有的babel-loader,并在plugins中添加了HappyPack的插件,将原有的babel-loader连同它的配置插入进去即可。

多个loader优化

在使用HappyPack优化多个loader时,需要为每一个loader配置一个id,否则HappyPack无法知道rules与plugins如何一一对应。

const HappyPack = require('happypack'); module.exports = { //... module: { rules: [ { test: /\.js$/, exclude: /node_modules/, loader: 'happypack/loader?id=js', }, { test: /\.ts$/, exclude: /node_modules/, loader: 'happypack/loader?id=ts', } ], }, plugins: [ new HappyPack({ id: 'js', loaders: [{ loader: 'babel-loader', options: {}, // babel options }], }), new HappyPack({ id: 'ts', loaders: [{ loader: 'ts-loader', options: {}, // ts options }], }) ] }; webpack-bundle-analyzer (图形化分析bundle的构成和内存) npm i webpack-bundle-analyzer -D const Analyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { // ... plugins: [ new Analyzer() ], };

npx webpack 占据终端并自动打开浏览器

webpack-dashboard (更直观的看到控制台的打包信息) npm i webpack-dashboard -D const DashboardPlugin = require('webpack-dashboard/plugin'); plugins:[new DashboardPlugin()],

修改package.json

为了使webpack-dashboard生效还要更改一下webpack的启动方式,就是用webpack-dashboard模块命令替代原本的webpack或者webpack-dev-server的命令,并将原有的启动命令作为参数传给它。如:

"dev": "webpack-dashboard -- webpack-dev-server"

多个webpack配置文件 (webpack-merge)

多个环境下的webpack配置文件如何切换?

这些名字是可以随意取得 webpack默认只认识webpack.config.js;

npx webpack --config webpack.dev.config

公共的配置提取出来,webpack-merge是配置合并的工具。

npm i webpack-merge -D

webpack.common.js

// webpack.common.js module.exports = { entry: './app.js', output: { filename: '[name].js', }, module: { rules: [ { test: /\.(png|jpg|gif)$/, use: 'file-loader', }, { test: /\.css$/, use: [ 'style-loader', 'css-loader' ], } ], }, };

webpack.prod.js 重写对 test: /\.css$/规则的处理

// webpack.prod.js const merge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = merge.smart(commonConfig, { mode: 'production', module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', // css-loader 负责解析 CSS 代码, 处理 CSS 中的依赖 ], } ] }, plugins: [ // 用 MiniCssExtractPlugin 抽离出 css 文件 new MiniCssExtractPlugin({ filename: '[name].bundle.css' // 输出的 css 文件名为 index.css }), ] });

参考文档: 《webpack实战 入门进阶与调优》 webpack官网 webpack 中,module,chunk 和 bundle 的区别是什么?



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3